/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package TransportLayer;

import DataModel.INotaryPolicy;
import DataModel.NotaryCertificate;
import DataModel.NotaryServerAddress;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.joda.time.DateTime;

public abstract class ServiceClient implements IServiceClient
{
	protected INotaryPolicy notaryPolicy;
	protected NotaryServerAddress[] notaryServerAddresses;
	protected DatagramSocket socket;

	private final String registerTimer = "register";

	public ServiceClient( INotaryPolicy notaryPolicy, NotaryServerAddress[] NotaryServerAddresses ) throws SocketException
	{
		this.notaryPolicy = notaryPolicy;
		this.notaryServerAddresses = NotaryServerAddresses;
		socket = new DatagramSocket();
	}

	public INotaryPolicy getNotaryPolicy()
	{
		return notaryPolicy;
	}

	@Override
	public void setNotaryPolicy( INotaryPolicy notaryPolicy )
	{
		try
		{
			this.notaryPolicy = notaryPolicy;
			this.socket.setSoTimeout(this.notaryPolicy.getSocketTimeout());
		}
		catch ( SocketException ex )
		{
			Logger.getLogger(ServiceClient.class.getName()).log(Level.SEVERE, null, ex);
		}
	}

	public NotaryServerAddress[] getNotaryServerAddresses()
	{
		return notaryServerAddresses;
	}

	@Override
	public void setNotaryServerAddresses( NotaryServerAddress[] NotaryServerAddresses )
	{
		this.notaryServerAddresses = NotaryServerAddresses;
	}

	@Override
	public ArrayList<NotaryResponse> verify( final NotaryCertificate notaryCertificate )
	{
		int[] indexes = GenerateIndexes();
		ExecutorService executor = Executors.newCachedThreadPool();
		List<Callable<NotaryResponse>> tasks = Collections.synchronizedList(new ArrayList<Callable<NotaryResponse>>());

		final List<NotaryResponse> resultArray = Collections.synchronizedList(new ArrayList<NotaryResponse>());
		for ( final int i: indexes )
		{
			tasks.add(new Callable<NotaryResponse>()
			{
				@Override
				public NotaryResponse call() throws Exception
				{
					try
					{
						NotaryRequest request = new NotaryRequest(NotaryRequestType.Verification, notaryCertificate);

						ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

						ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);

						objectOutputStream.writeObject(request);
						objectOutputStream.flush();

						byte[] buffer = byteArrayOutputStream.toByteArray();

						byte[] packetbuffer = new byte[buffer.length + 2];

						byte[] lengthArray = ByteBuffer.allocate(4).putInt(buffer.length).array();

						packetbuffer[0] = lengthArray[2];
						packetbuffer[0] = lengthArray[3];

						DatagramPacket packet = new DatagramPacket(packetbuffer, packetbuffer.length,
																											 notaryServerAddresses[i].getAddress(),
																											 notaryServerAddresses[i].getPort());
						socket.send(packet);

						byte[] packetLenghtArray = new byte[2];

						packet = new DatagramPacket(packetLenghtArray, packetLenghtArray.length);
						socket.receive(packet);

						int packetlength = ( int ) packet.getData()[1] + ( int ) packet.getData()[0] * 256;

						byte[] packetArray = new byte[packetlength];
						packet = new DatagramPacket(packetArray, packetArray.length);
						socket.receive(packet);

						ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(packet.getData());
						ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
						return ( NotaryResponse ) objectInputStream.readObject();
					}
					catch ( IOException | ClassNotFoundException ex )
					{
						Logger.getLogger(ServiceClient.class.getName()).log(Level.SEVERE, null, ex);
						return null;
					}
				}
			});
		}
		try
		{
			List<Future<NotaryResponse>> results = executor.invokeAll(tasks, this.notaryPolicy.getOperationTimeout(),
																																TimeUnit.MILLISECONDS);
			for ( int i = 0; i < resultArray.size(); ++i )
			{
				executor.submit(new TaskChecker(i, resultArray, results, notaryPolicy));
			}
		}
		catch ( InterruptedException ex )
		{
			Logger.getLogger(ServiceClient.class.getName()).log(Level.SEVERE, null, ex);
		}
		return null;
	}

	@Override
	public NotaryResponse register( final NotaryCertificate notaryCertificate ) throws InterruptedException, ExecutionException
	{
		ExecutorService executor = Executors.newSingleThreadExecutor();
		Future<NotaryResponse> future = executor.submit(new Callable<NotaryResponse>()
		{
			@Override
			public NotaryResponse call() throws Exception
			{
				try
				{
					NotaryRequest request = new NotaryRequest(NotaryRequestType.Registration, notaryCertificate);

					ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

					ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);

					objectOutputStream.writeObject(request);
					objectOutputStream.flush();

					byte[] buffer = byteArrayOutputStream.toByteArray();

					byte[] packetbuffer = new byte[buffer.length + 2];

					byte[] lengthArray = ByteBuffer.allocate(4).putInt(buffer.length).array();

					packetbuffer[0] = lengthArray[2];
					packetbuffer[0] = lengthArray[3];

					Random random = new Random(DateTime.now().getMillis());
					int idx = random.nextInt(notaryServerAddresses.length);

					DatagramPacket packet = new DatagramPacket(packetbuffer, packetbuffer.length,
																										 notaryServerAddresses[idx].getAddress(),
																										 notaryServerAddresses[idx].getPort());
					socket.send(packet);

					byte[] packetLenghtArray = new byte[2];

					packet = new DatagramPacket(packetLenghtArray, packetLenghtArray.length);
					socket.receive(packet);

					int packetlength = ( int ) packet.getData()[1] + ( int ) packet.getData()[0] * 256;

					byte[] packetArray = new byte[packetlength];
					packet = new DatagramPacket(packetArray, packetArray.length);
					socket.receive(packet);

					ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(packet.getData());
					ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
					return ( NotaryResponse ) objectInputStream.readObject();
				}
				catch ( IOException | ClassNotFoundException ex )
				{
					Logger.getLogger(ServiceClient.class.getName()).log(Level.SEVERE, null, ex);
					return null;
				}
			}
		});
		try
		{
			return future.get(this.notaryPolicy.getOperationTimeout(), TimeUnit.MILLISECONDS);
		}
		catch ( TimeoutException ex )
		{
			System.err.println("timeout");
			return null;
		}
	}

	@Override
	public NotaryResponse revoke( NotaryCertificate notaryCertificate )
	{
		throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
	}

	private int[] GenerateIndexes()
	{
		int[] result = new int[this.notaryPolicy.getNotariesToCheck()];
		Random random = new Random(DateTime.now().getMillis());

		for ( int i = 0; i < notaryPolicy.getNotariesToCheck() - notaryPolicy.getNotariesToSecondCheck(); ++i )
		{
			result[i] = random.nextInt(notaryServerAddresses.length);
		}
		for ( int i = notaryPolicy.getNotariesToCheck() - notaryPolicy.getNotariesToSecondCheck(); i < result.length; ++i )
		{
			result[i] = result[random.nextInt(notaryPolicy.getNotariesToSecondCheck())];
		}
		return result;
	}
}
